﻿
namespace CmdArgs
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using CmdArgs.Settings;
    using CmdArgs.Validation;

    /// <summary>    
    /// The CommandLineParser provides methods for parsing command line arguments.
    /// </summary>
    /// <remarks>
    /// Command line arguments processing is divided into 3 parts.
    /// <list type="bullet">
    ///     <item>
    ///         <description>
    ///         First is creating settings for parser typically using 
    ///         <see cref="SettingsBuilder"/>, this is optional.
    ///         </description>
    ///     </item>
    ///     
    ///     <item>
    ///         <description>
    ///         Second is validation using method <see cref="Validate"/>.
    ///         Validation returns validation result with errors list.
    ///         </description>
    ///     </item>
    ///     
    ///     <item>
    ///         <description>
    ///         Third is parsing itself using method <see cref="Parse"/>.
    ///         This method returns <see cref="CommandLineArgs"/> object 
    ///         which is data structure for parsed options and arguments.
    ///         </description>
    ///     </item>
    /// </list>
    /// It may seem that parsing of command line argument happens two 
    /// times during this process (validating and parsing itself), 
    /// but under the hood: result of first one is cached.
    /// </remarks>
    public class CommandLineParser
    {
        private const string ArgumentSeparator = "--";
        private const string LongOptionPrefix = ArgumentSeparator;
        private const string ShortOptionPrefix = "-";
        private const string OptionParameterAssignSign = "=";

        private Settings.Settings settings;
        private IDictionary<string[], CacheItem> cache = new Dictionary<string[], CacheItem>();

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandLineParser"/> class.
        /// </summary>
        /// <param name="settings">The settings for validation and parsing.</param>
        public CommandLineParser(Settings.Settings settings)
        {
            this.settings = settings;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="CommandLineParser"/> class without settings.
        /// Initializing this class without settings is possible, 
        /// but in most cases initilizing it with some settings is more suitable.
        /// </summary>
        public CommandLineParser()
        {
            this.settings = new Settings.Settings(new OptionSettings[0]);
        }

        /// <summary>
        /// Validates the specified command line arguments 
        /// against validators configured in settings.
        /// Settings are parameter of constructor of this class.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        /// <returns>Validation result with list of errors.</returns>
        public ValidationResult Validate(string[] args)
        {
            this.Parse(args);   
            System.Diagnostics.Debug.Assert(
                this.cache.ContainsKey(args),
                "No validation result after parsing.");

            return this.cache[args].ValidationResult;
        }

        /// <summary>
        /// Parses the specified command line arguments.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        /// <returns><see cref="CommandLineArgs"/> object with parsed options, parameters and arguments.</returns>
        public CommandLineArgs Parse(string[] args)
        {
            if (this.cache.ContainsKey(args))
            {
                return this.cache[args].ParseResult;
            }

            var parsedArguments = new List<string>();
            var options = new OptionsDictionary();
            var validationResultList = new List<ValidationResult>();

            int i = 0;
            while (i < args.Length)
            {
                string currentArgument = args[i];

                if (currentArgument.Equals(ArgumentSeparator))
                {
                    // Only arguments follows
                    i++;
                    parsedArguments.AddRange(this.ParseArguments(args, i, out i));
                }
                else if (currentArgument.StartsWith(LongOptionPrefix))
                {
                    // Long option and its parameters
                    ValidationResult optionValidationResult;
                    var option = this.ParseLongOption(args, i, out i, out optionValidationResult);
                    if (option == null)
                    {
                        parsedArguments.Add(currentArgument);
                        i++;
                    }
                    else
                    {
                        options.AddOption(option);
                        validationResultList.Add(optionValidationResult);
                    }
                }
                else if (currentArgument.StartsWith(ShortOptionPrefix))
                {
                    // Short options without parameters or single options with its parameters
                    ValidationResult optionValidationResult;
                    var shortOptions = this.ParseShortOption(args, i, out i, out optionValidationResult);
                    if (shortOptions == null)
                    {
                        parsedArguments.Add(currentArgument);
                        i++;
                    }
                    else
                    {
                        foreach (var option in shortOptions)
                        {
                            options.AddOption(option);
                        }

                        validationResultList.Add(optionValidationResult);
                    }
                }
                else
                {
                    // Argument
                    parsedArguments.Add(currentArgument);
                    i++;
                }
            }

            var parsedArgs = new CacheItem();
            parsedArgs.ParseResult = new CommandLineArgs(options, parsedArguments.ToArray());
            parsedArgs.ValidationResult = new ValidationResult(validationResultList);

            this.cache.Add(args, parsedArgs);

            return parsedArgs.ParseResult;
        }

        private static List<string> ParseOptionParams(string[] args, int startIdx, OptionSettings optionSettings,
            List<string> preparsedParameters, out int actualIdx, out ValidationError error)
        {
            actualIdx = startIdx;
            error = null;
            var optionParams = new List<string>();
            optionParams.AddRange(preparsedParameters);

            while (optionSettings.ParametersCount.Satisfies(optionParams.Count))
            {
                if (actualIdx >= args.Length || args[actualIdx].Equals(ArgumentSeparator))
                {
                    break;
                }                

                optionParams.Add(args[actualIdx]);
                actualIdx++;
            }

            if (!string.IsNullOrEmpty(optionSettings.DefaultValue)
                && (optionParams.Count <= 0))
            {
                optionParams.Add(optionSettings.DefaultValue);
            }

            if (!optionSettings.ParametersCount.IsValidCount(optionParams.Count))
            {
                error = new ValidationError(optionSettings.ParametersCount.ErrorMessage, optionSettings);
            }

            return optionParams;
        }

        private void SeparateOptionNameAndParameter(string rawName, out string optionName, out string parameter)
        {
            var assignSignIdx = rawName.IndexOf(OptionParameterAssignSign);

            parameter = rawName.Substring(assignSignIdx + 1);
            optionName = rawName.Substring(0, assignSignIdx);
        }

        private List<Option> ParseShortOption(
            string[] args, 
            int startIdx, 
            out int actualIdx,
            out ValidationResult validationResult)
        {
            validationResult = null;
            string optionName = args[startIdx].Substring(ShortOptionPrefix.Length);
            var returnedOptionList = new List<Option>(optionName.Length);
            var optionParams = new List<string>();

            if (optionName.Contains(OptionParameterAssignSign))
            {
                string firstParameter;
                this.SeparateOptionNameAndParameter(optionName, out optionName, out firstParameter);
                optionParams.Add(firstParameter);
            }

            if (optionName.Length == 0)
            {
                actualIdx = startIdx;
                return null;
            }
            else if (optionName.Length == 1)
            {
                // One short option
                OptionSettings optionSettings = this.settings.GetOptionSettings(optionName);

                if (optionSettings == null)
                {
                    // No such an option defined - its an argument
                    actualIdx = startIdx;
                    return null;
                }

                ValidationError error;
                optionParams = ParseOptionParams(args, startIdx + 1, optionSettings, optionParams, out actualIdx, out error);

                validationResult = optionSettings.Validate(optionParams);

                if (error != null)
                {
                    var valResultList = new List<ValidationResult>(2);

                    valResultList.Add(validationResult);
                    valResultList.Add(new ValidationResult(error));

                    validationResult = new ValidationResult(valResultList);
                }


                returnedOptionList.Add(new Option(optionParams, optionSettings));
            }
            else
            {
                // Grouped short list - no option parameters allowed
                if (optionParams.Count != 0)
                {
                    // No parameters after assign sign allowed -> its an argument
                    actualIdx = startIdx;
                    return null;
                }

                returnedOptionList = this.ParseGroupedShortOption(optionName, startIdx, out actualIdx, out validationResult);
            }

            return returnedOptionList;
        }

        /// <summary>
        /// Parses grouped short options with no parameters
        /// </summary>
        /// <returns>null - some options hasn't been recognized (assuming it's an argument)</returns>
        private List<Option> ParseGroupedShortOption(
            string optionName, 
            int startIdx, 
            out int actualIdx,
            out ValidationResult validationResult)
        {
            char[] optionArray = optionName.ToCharArray();
            actualIdx = startIdx + 1;
            var validationResultList = new List<ValidationResult>();

            var returnedOptionList = new List<Option>();
            var errorOptionList = new List<OptionSettings>();
            for (int i = 0; i < optionArray.Length; i++)
            {
                OptionSettings optionSettings = this.settings.GetOptionSettings(optionArray[i].ToString());

                if (optionSettings == null)
                {
                    // No such an option defined - whole optionArray/optionName its an argument
                    actualIdx = startIdx;
                    validationResult = null;
                    return null;
                }

                validationResultList.Add(optionSettings.Validate(new List<string>()));

                if (!optionSettings.ParametersCount.IsValidCount(0))
                {
                    // Grouped short options can't have any parameters
                    validationResultList.Add(
                        new ValidationResult(
                            new ValidationError(
                                Validation.ErrorMessages.GroupedShortOptionsWithParametersNotAllowed, 
                                optionSettings)));
                }

                returnedOptionList.Add(new Option(optionSettings));
            }

            validationResult = new ValidationResult(validationResultList);

            return returnedOptionList;
        }

        private Option ParseLongOption(
            string[] args, 
            int startIdx, 
            out int actualIdx,
            out ValidationResult validationResult)
        {
            string optionName = args[startIdx].Substring(LongOptionPrefix.Length);
            validationResult = null;

            var optionParams = new List<string>();

            if (optionName.Contains(OptionParameterAssignSign))
            {
                string firstParameter;
                this.SeparateOptionNameAndParameter(optionName, out optionName, out firstParameter);
                optionParams.Add(firstParameter);
            }

            OptionSettings optionSettings = this.settings.GetOptionSettings(optionName);

            if (optionSettings == null)            
            {
                // No such an option defined - its an argument
                actualIdx = startIdx;
                return null;
            }

            ValidationError error;
            optionParams = ParseOptionParams(args, startIdx + 1, optionSettings, optionParams, out actualIdx,
                out error);

            validationResult = optionSettings.Validate(optionParams);

            if (error != null)
            {
                var valResultList = new List<ValidationResult>(2);

                valResultList.Add(validationResult);
                valResultList.Add(new ValidationResult(error));

                validationResult = new ValidationResult(valResultList);
            }

            return new Option(optionParams, optionSettings);
        }

        private List<string> ParseArguments(string[] args, int startIdx, out int actualIdx)
        {
            var parsedArguments = new List<string>();
            actualIdx = startIdx;
            while (actualIdx < args.Length)
            {
                parsedArguments.Add(args[actualIdx]);
                actualIdx++;
            }

            return parsedArguments;
        }

        private class CacheItem
        {
            public CommandLineArgs ParseResult { get; set; }

            public ValidationResult ValidationResult { get; set; }
        }
    }
}
